package sqltool

import (
	"fmt"
	"go/format"
	"strings"
)

type GenUnitTest struct {
	pkg      string
	codeInfo GoCodeInfo
}

func NewGenUnitTest(code string) *GenUnitTest {
	gc, _ := ParseGo(code)
	return &GenUnitTest{
		pkg:      gc.PkgName,
		codeInfo: gc,
	}
}

func (gen GenUnitTest) Code() string {
	codes := make([]string, 0)
	codes = append(codes, gen.PackageBlock())
	codes = append(codes, gen.ImportBlock())
	codes = append(codes, strings.Join(gen.Funcs(), "\n\n"))
	mergeCode := strings.Join(codes, "\n\n")
	return gofmt(mergeCode)
}

func (gen GenUnitTest) Funcs() []string {
	testFuncName := func(fnName, recvType string) string {
		if recvType == "" {
			return fmt.Sprintf("Test%s", fnName)
		} else {
			return fmt.Sprintf("Test%s_%s", strings.Replace(recvType, "*", "", -1), fnName)
		}
	}
	rets := make([]string, 0)
	structDef := func(fieldNames, fieldTypes []string) string {
		fieldLines := make([]string, 0)
		for i, _ := range fieldNames {
			fieldLines = append(fieldLines, fmt.Sprintf("%s %s", fieldNames[i], fieldTypes[i]))
		}
		return fmt.Sprintf("struct{\n%s\n}", strings.Join(fieldLines, "\n"))
	}
	getStruct := func(recvType string) *GoStruct {
		for _, st := range gen.codeInfo.Structs {
			if st.Name == recvType[len(recvType)-len(st.Name):] {
				return &st
			}
		}
		return nil
	}
	joinWithoutEmpty := func(sep string, elems ...string) string {
		var es []string
		for _, e := range elems {
			if e != "" {
				es = append(es, e)
			}
		}
		return strings.Join(es, sep)
	}
	for _, fn := range gen.codeInfo.Funcs {
		fnDecl := fmt.Sprintf("func %s(t *testing.T)", testFuncName(fn.Name, fn.RecvType))

		var fieldDefBlock string
		{
			if fn.RecvType != "" {
				st := getStruct(fn.RecvType)
				fieldDefBlock = fmt.Sprintf("type fields %s", structDef(st.FieldNames, st.FieldTypes))
			}
		}

		var argsDefBlock string
		{
			if len(fn.ParamTypes) > 0 {
				argsDefBlock = fmt.Sprintf("type args %s", structDef(fn.ParamNames, fn.ParamTypes))
			}
		}

		var testsBlock string
		var gotType string
		var errInRet bool
		{
			testsStructLines := make([]string, 0)
			testsStructLines = append(testsStructLines, fmt.Sprintf("%s %s", "name", "string"))
			if fieldDefBlock != "" {
				testsStructLines = append(testsStructLines, fmt.Sprintf("%s %s", "fields", "fields"))
			}
			if argsDefBlock != "" {
				testsStructLines = append(testsStructLines, fmt.Sprintf("%s %s", "args", "args"))
			}
			for _, rt := range fn.RetTypes {
				if rt == "error" {
					errInRet = true
				}
			}
			if len(fn.RetTypes) == 2 || (len(fn.RetTypes) == 1 && !errInRet) {
				gotType = fn.RetTypes[0]
			}
			if gotType != "" {
				testsStructLines = append(testsStructLines, fmt.Sprintf("%s %s", "want", gotType))
			}
			if errInRet {
				testsStructLines = append(testsStructLines, fmt.Sprintf("%s %s", "wantErr", "bool"))
			}
			testsStruct := fmt.Sprintf("struct{\n%s\n}", strings.Join(testsStructLines, "\n"))
			var testsBody string
			testsBlock = fmt.Sprintf("tests := []%s{\n%s\n}", testsStruct, testsBody)
		}

		var retCheckBlock string
		runBlocks := make([]string, 0)
		{
			var recvInstance string
			recvRef := "self"
			if fn.RecvType != "" {
				st := getStruct(fn.RecvType)
				fLines := make([]string, 0)
				for _, fld := range st.FieldNames {
					fLines = append(fLines, fmt.Sprintf("%s:tt.fields.%s,\n", fld, fld))
				}
				recvInstance = fmt.Sprintf("%s := &%s{\n%s\n}", recvRef, st.Name, strings.Join(fLines, ""))
				runBlocks = append(runBlocks, recvInstance)
			}
			ttArgs := make([]string, 0)
			for _, name := range fn.ParamNames {
				ttArgs = append(ttArgs, fmt.Sprintf("tt.args.%s", name))
			}
			var leftRets []string
			if gotType != "" && errInRet {
				leftRets = []string{"got", "err"}
			} else if gotType == "" && errInRet {
				leftRets = []string{"err"}
			} else if gotType != "" && !errInRet {
				leftRets = []string{"got"}
			}
			var equalMark string
			if len(leftRets) > 0 {
				equalMark = ":="
			}
			var recvRefPrefix string
			if recvInstance != "" {
				recvRefPrefix = recvRef + "."
			}
			funcCallLine := fmt.Sprintf("%s %s %s %s(%s)", strings.Join(leftRets, ", "), equalMark, recvRefPrefix, fn.Name, strings.Join(ttArgs, ", "))
			runBlocks = append(runBlocks, funcCallLine)
			if errInRet {
				errBlock := fmt.Sprintf("if (err != nil) != tt.wantErr {\nt.Errorf(\"%s() error = %%v, wantErr %%v\", err, tt.wantErr)\nreturn\n}", fn.Name)
				runBlocks = append(runBlocks, errBlock)
			}
			if gotType != "" {
				gotEqualCheck := fmt.Sprintf("if !reflect.DeepEqual(got, tt.want) {\nt.Errorf(\"%s() got = %%v, want %%v\", got, tt.want)\n}", fn.Name)
				runBlocks = append(runBlocks, gotEqualCheck)
			}
			retCheckBlock = fmt.Sprintf("for _, tt := range tests {\nt.Run(tt.name, func(t *testing.T) {\n%s\n})\n}", strings.Join(runBlocks, "\n"))
		}
		rets = append(rets, fmt.Sprintf("%s {\n%s\n}", fnDecl, joinWithoutEmpty("\n", fieldDefBlock, argsDefBlock, testsBlock, retCheckBlock)))
	}
	return rets
}

func (gen GenUnitTest) PackageBlock() string {
	return fmt.Sprintf("package %s", gen.pkg)
}

func (gen GenUnitTest) ImportBlock() string {
	paths := make([]string, 0)
	paths = append(paths, "reflect", "testing")
	dPaths := make([]string, 0)
	for _, p := range paths {
		dPaths = append(dPaths, fmt.Sprintf(`"%s"`, p))
	}
	return fmt.Sprintf(`import (
    %s
)`, strings.Join(dPaths, "\n"))
}

func gofmt(code string) string {
	formatted, _ := format.Source([]byte(code))
	return string(formatted)
}